YAHOO.namespace("tool");

/**
 * The YUI JavaScript profiler.
 * @module profiler
 * @namespace YAHOO.tool
 * @requires yahoo
 */

/**
 * Profiles functions in JavaScript.
 * @namespace YAHOO.tool
 * @class Profiler
 * @static
 */
YAHOO.tool.Profiler = function(){


    //-------------------------------------------------------------------------
    // Private Variables and Functions
    //-------------------------------------------------------------------------
    
    var container   = {},   //Container object on which to put the original unprofiled methods.
        report      = {},   //Profiling information for functions
        stopwatches = {},   //Additional stopwatch information
        
        WATCH_STARTED   = 0,
        WATCH_STOPPED   = 1,
        WATCH_PAUSED    = 2,    
        
        lang    = YAHOO.lang;

    /**
     * Creates a report object with the given name.
     * @param {String} name The name to store for the report object.
     * @return {Void}
     * @method createReport
     * @private
     */
    function createReport(name){
        report[name] = {
            calls: 0,
            max: 0,
            min: 0,
            avg: 0,
            points: []
        };      
    }
    
    /**
     * Called when a method ends execution. Marks the start and end time of the 
     * method so it can calculate how long the function took to execute. Also 
     * updates min/max/avg calculations for the function.
     * @param {String} name The name of the function to mark as stopped.
     * @param {int} duration The number of milliseconds it took the function to
     *      execute.
     * @return {Void}
     * @method saveDataPoint
     * @private
     * @static
     */
    function saveDataPoint(name, duration){

        //get the function data
        var functionData /*:Object*/ = report[name];
        
        //just in case clear() was called
        if (!functionData){
            functionData = createReport(name);
        }
    
        //increment the calls
        functionData.calls++;
        functionData.points.push(duration);

        //if it's already been called at least once, do more complex calculations
        if (functionData.calls > 1) {
            functionData.avg = ((functionData.avg*(functionData.calls-1))+duration)/functionData.calls;
            functionData.min = Math.min(functionData.min, duration);
            functionData.max = Math.max(functionData.max, duration);
        } else {
            functionData.avg = duration;
            functionData.min = duration;
            functionData.max = duration;
        }                             
    
    }

    //-------------------------------------------------------------------------
    // Singleton Object
    //-------------------------------------------------------------------------
    
    return {
    
        //-------------------------------------------------------------------------
        // Utility Methods
        //-------------------------------------------------------------------------        
        
        /**
         * Removes all report data from the profiler.
         * @param {String} name (Optional) The name of the report to clear. If
         *      omitted, then all report data is cleared.
         * @return {Void}
         * @method clear
         * @static
         */
        clear: function(name){
            if (lang.isString(name)){
                delete report[name];
                delete stopwatches[name];
            } else {
                report = {};
                stopwatches = {};
            }
        },

        /**
         * Returns the uninstrumented version of a function/object.
         * @param {String} name The name of the function/object to retrieve.
         * @return {Function|Object} The uninstrumented version of a function/object.
         * @method getOriginal
         * @static
         */    
        getOriginal: function(name){
            return container[name];
        },
    
        /**
         * Instruments a method to have profiling calls.
         * @param {String} name The name of the report for the function.
         * @param {Function} method The function to instrument.
         * @return {Function} An instrumented version of the function.
         * @method instrument
         * @static
         */
        instrument: function(name, method){
        
            //create instrumented version of function
            var newMethod = function () {
    
                var start = new Date(),
                    retval = method.apply(this, arguments),
                    stop = new Date();
                
                saveDataPoint(name, stop-start);
                
                return retval;                
            
            };     

            //copy the function properties over
            lang.augmentObject(newMethod, method);
            
            //assign prototype and flag as being profiled
            newMethod.__yuiProfiled = true;
            newMethod.prototype = method.prototype;
            
            //store original method
            container[name] = method;
            container[name].__yuiFuncName = name;
            
            //create the report
            createReport(name);

            //return the new method
            return newMethod;
        },    
        
        //-------------------------------------------------------------------------
        // Stopwatch Methods
        //-------------------------------------------------------------------------        
        
        /**
         * Pauses profiling information for a given name.
         * @param {String} name The name of the data point.
         * @return {Void}
         * @method pause
         * @static
         */        
        pause: function(name){
            var now = new Date(),
                stopwatch = stopwatches[name];
                
            if (stopwatch && stopwatch.state == WATCH_STARTED){
                stopwatch.total += (now - stopwatch.start);
                stopwatch.start = 0;
                stopwatch.state = WATCH_PAUSED;
            }
        
        },
        
        /**
         * Start profiling information for a given name. The name cannot be the name
         * of a registered function or object. This is used to start timing for a
         * particular block of code rather than instrumenting the entire function.
         * @param {String} name The name of the data point.
         * @return {Void}
         * @method start
         * @static
         */
        start: function(name){
            if(container[name]){
                throw new Error("Cannot use '" + name + "' for profiling through start(), name is already in use.");
            } else {
            
                //create report if necessary
                if (!report[name]){
                    createReport(name);
                }
                
                //create stopwatch object if necessary
                if (!stopwatches[name]){             
                    stopwatches[name] = {
                        state: WATCH_STOPPED,
                        start: 0,
                        total: 0
                    };
                }
                
                if (stopwatches[name].state == WATCH_STOPPED){
                    stopwatches[name].state = WATCH_STARTED;
                    stopwatches[name].start = new Date();                    
                }

            }
        },
        
        /**
         * Stops profiling information for a given name.
         * @param {String} name The name of the data point.
         * @return {Void}
         * @method stop
         * @static
         */
        stop: function(name){
            var now = new Date(),
                stopwatch = stopwatches[name];
                
            if (stopwatch){
                if (stopwatch.state == WATCH_STARTED){
                    saveDataPoint(name, stopwatch.total + (now - stopwatch.start));                    
                } else if (stopwatch.state == WATCH_PAUSED){
                    saveDataPoint(name, stopwatch.total);
                }
                
                //reset stopwatch information
                stopwatch.start = 0;
                stopwatch.total = 0;
                stopwatch.state = WATCH_STOPPED;                
            }
        },
    
        //-------------------------------------------------------------------------
        // Reporting Methods
        //-------------------------------------------------------------------------    
        
        /**
         * Returns the average amount of time (in milliseconds) that the function
         * with the given name takes to execute.
         * @param {String} name The name of the function whose data should be returned.
         *      If an object type method, it should be 'constructor.prototype.methodName';
         *      a normal object method would just be 'object.methodName'.
         * @return {float} The average time it takes the function to execute.
         * @method getAverage
         * @static
         */
        getAverage : function (name /*:String*/) /*:float*/ {
            return report[name].avg;
        },
    
        /**
         * Returns the number of times that the given function has been called.
         * @param {String} name The name of the function whose data should be returned.
         * @return {int} The number of times the function was called.
         * @method getCallCount
         * @static
         */
        getCallCount : function (name /*:String*/) /*:int*/ {
            return report[name].calls;    
        },
        
        /**
         * Returns the maximum amount of time (in milliseconds) that the function
         * with the given name takes to execute.
         * @param {String} name The name of the function whose data should be returned.
         *      If an object type method, it should be 'constructor.prototype.methodName';
         *      a normal object method would just be 'object.methodName'.
         * @return {float} The maximum time it takes the function to execute.
         * @method getMax
         * @static
         */
        getMax : function (name /*:String*/) /*:int*/ {
            return report[name].max;
        },
        
        /**
         * Returns the minimum amount of time (in milliseconds) that the function
         * with the given name takes to execute.
         * @param {String} name The name of the function whose data should be returned.
         *      If an object type method, it should be 'constructor.prototype.methodName';
         *      a normal object method would just be 'object.methodName'.
         * @return {float} The minimum time it takes the function to execute.
         * @method getMin
         * @static
         */
        getMin : function (name /*:String*/) /*:int*/ {
            return report[name].min;
        },
    
        /**
         * Returns an object containing profiling data for a single function.
         * The object has an entry for min, max, avg, calls, and points).
         * @return {Object} An object containing profile data for a given function.
         * @method getFunctionReport
         * @static
         * @deprecated Use getReport() instead.
         */
        getFunctionReport : function (name /*:String*/) /*:Object*/ {
            return report[name];
        },
    
        /**
         * Returns an object containing profiling data for a single function.
         * The object has an entry for min, max, avg, calls, and points).
         * @return {Object} An object containing profile data for a given function.
         * @method getReport
         * @static
         */
        getReport : function (name /*:String*/) /*:Object*/ {
            return report[name];
        },
    
        /**
         * Returns an object containing profiling data for all of the functions 
         * that were profiled. The object has an entry for each function and 
         * returns all information (min, max, average, calls, etc.) for each
         * function.
         * @return {Object} An object containing all profile data.
         * @static
         */
        getFullReport : function (filter /*:Function*/) /*:Object*/ {
            filter = filter || function(){return true;};
        
            if (lang.isFunction(filter)) {
                var fullReport = {};
                
                for (var name in report){
                    if (filter(report[name])){
                        fullReport[name] = report[name];    
                    }
                }
                
                return fullReport;
            }
        },
    
        //-------------------------------------------------------------------------
        // Profiling Methods
        //-------------------------------------------------------------------------   
        
        /**
         * Sets up a constructor for profiling, including all properties and methods on the prototype.
         * @param {string} name The fully-qualified name of the function including namespace information.
         * @param {Object} owner (Optional) The object that owns the function (namespace or containing object).
         * @return {Void}
         * @method registerConstructor
         * @static
         */
        registerConstructor : function (name /*:String*/, owner /*:Object*/) /*:Void*/ {    
            this.registerFunction(name, owner, true);
        },
    
        /**
         * Sets up a function for profiling. It essentially overwrites the function with one
         * that has instrumentation data. This method also creates an entry for the function
         * in the profile report. The original function is stored on the container object.
         * @param {String} name The full name of the function including namespacing. This
         *      is the name of the function that is stored in the report.
         * @param {Object} owner (Optional) The object that owns the function. If the function
         *      isn't global then this argument is required. This could be the namespace that
         *      the function belongs to, such as YAHOO.util.Dom, or the object on which it's
         *      a method.
         * @param {Boolean} registerPrototype (Optional) Indicates that the prototype should
         *      also be instrumented. Setting to true has the same effect as calling
         *      registerConstructor().
         * @return {Void}
         * @method registerFunction
         * @static
         */     
        registerFunction : function(name /*:String*/, owner /*:Object*/, registerPrototype /*:Boolean*/) /*:Void*/{
        
            //figure out the function name without namespacing
            var funcName = (name.indexOf(".") > -1 ? 
                    name.substring(name.lastIndexOf(".")+1) : name),
                method,
                prototype;
                
            //if owner isn't an object, try to find it from the name
            if (!lang.isObject(owner)){
                owner = eval(name.substring(0, name.lastIndexOf(".")));
            }
            
            //get the method and prototype
            method = owner[funcName];
            prototype = method.prototype;
            
            //see if the method has already been registered
            if (lang.isFunction(method) && !method.__yuiProfiled){
                
                //replace the function with the profiling one
                owner[funcName] = this.instrument(name, method);
                        
                /*
                 * Store original function information. We store the actual
                 * function as well as the owner and the name used to identify
                 * the function so it can be restored later.
                 */
                container[name].__yuiOwner = owner;
                container[name].__yuiFuncName = funcName;  //overwrite with less-specific name
                 
                //register prototype if necessary
                if (registerPrototype) {            
                    this.registerObject(name + ".prototype", prototype);          
                }
    
            }
        
        },
            
        
        /**
         * Sets up an object for profiling. It takes the object and looks for functions.
         * When a function is found, registerMethod() is called on it. If set to recrusive
         * mode, it will also setup objects found inside of this object for profiling, 
         * using the same methodology.
         * @param {String} name The name of the object to profile (shows up in report).
         * @param {Object} owner (Optional) The object represented by the name.
         * @param {Boolean} recurse (Optional) Determines if subobject methods are also profiled.
         * @return {Void}
         * @method registerObject
         * @static
         */
        registerObject : function (name /*:String*/, object /*:Object*/, recurse /*:Boolean*/) /*:Void*/{
        
            //get the object
            object = (lang.isObject(object) ? object : eval(name));
        
            //save the object
            container[name] = object;
        
            for (var prop in object) {
                if (typeof object[prop] == "function"){
                    if (prop != "constructor" && prop != "superclass"){ //don't do constructor or superclass, it's recursive
                        this.registerFunction(name + "." + prop, object);
                    }
                } else if (typeof object[prop] == "object" && recurse){
                    this.registerObject(name + "." + prop, object[prop], recurse);
                }
            }
        
        },    
        
        /**
         * Removes a constructor function from profiling. Reverses the registerConstructor() method.
         * @param {String} name The full name of the function including namespacing. This
         *      is the name of the function that is stored in the report.
         * @return {Void}
         * @method unregisterFunction
         * @static
         */     
        unregisterConstructor : function(name /*:String*/) /*:Void*/{
                
            //see if the method has been registered
            if (lang.isFunction(container[name])){
                this.unregisterFunction(name, true);
            }    
        },
        
        /**
         * Removes function from profiling. Reverses the registerFunction() method.
         * @param {String} name The full name of the function including namespacing. This
         *      is the name of the function that is stored in the report.
         * @return {Void}
         * @method unregisterFunction
         * @static
         */     
        unregisterFunction : function(name /*:String*/, unregisterPrototype /*:Boolean*/) /*:Void*/{
                
            //see if the method has been registered
            if (lang.isFunction(container[name])){
            
                //check to see if you should unregister the prototype
                if (unregisterPrototype){
                    this.unregisterObject(name + ".prototype", container[name].prototype);
                }
                    
                //get original data
                var owner /*:Object*/ = container[name].__yuiOwner,
                    funcName /*:String*/ = container[name].__yuiFuncName;
                    
                //delete extra information
                delete container[name].__yuiOwner;
                delete container[name].__yuiFuncName;
                
                //replace instrumented function
                owner[funcName] = container[name];
                
                //delete supporting information
                delete container[name];          
            }
                
        
        },
        
        /**
         * Unregisters an object for profiling. It takes the object and looks for functions.
         * When a function is found, unregisterMethod() is called on it. If set to recrusive
         * mode, it will also unregister objects found inside of this object, 
         * using the same methodology.
         * @param {String} name The name of the object to unregister.
         * @param {Boolean} recurse (Optional) Determines if subobject methods should also be
         *      unregistered.
         * @return {Void}
         * @method unregisterObject
         * @static
         */
        unregisterObject : function (name /*:String*/, recurse /*:Boolean*/) /*:Void*/{
        
            //get the object
            if (lang.isObject(container[name])){            
                var object = container[name];    
            
                for (var prop in object) {
                    if (typeof object[prop] == "function"){
                        this.unregisterFunction(name + "." + prop);
                    } else if (typeof object[prop] == "object" && recurse){
                        this.unregisterObject(name + "." + prop, recurse);
                    }
                }
                
                delete container[name];
            }
        
        }
         

    };

}();  